/*
 * topological-hex, a program to compute combinatorial hexahedral meshes.
 *
 * Copyright (C) <2018> <Université catholique de Louvain (UCL), Belgique>
 *
 * List of the contributors to the development, description and complete
 * License: see LICENSE file.
 *
 * This program (topological-hex) is free software:
 * you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program (see COPYING file).  If not,
 * see <http://www.gnu.org/licenses/>.
 */

#include "client.h"

#include <string.h>
#include <unistd.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/select.h>

static
bool client_should_stay_open(void *data) {
  return !((client*)data)->should_stop;
}

error_code client_init(client *client, const char *server_name) {
  client->should_stop = false;
  client->buffer = NULL;

  struct addrinfo hints;
  memset(&hints, 0, sizeof(hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  struct addrinfo *server_info;
  if (getaddrinfo(server_name, SERVER_PORT, &hints, &server_info) != 0)
    return SYSTEM_ERROR;

  struct addrinfo *it;
  for (it = server_info; it != NULL; it = it->ai_next) {
    int fd = socket(it->ai_family, it->ai_socktype, it->ai_protocol);
    if (fd == -1) continue;

    if (connect(fd, it->ai_addr, it->ai_addrlen) == -1) {
      close(fd);
      continue;
    }

    client->socket = fd;
    break;
  }

  freeaddrinfo(server_info);

  if (it == NULL)
    return SYSTEM_ERROR;

  configure_keepalive(client->socket);

  const char *user_name = getenv("USER");
  if (!user_name) user_name = "unknown";

  if (!write_name(client_should_stay_open, client, client->socket,
                  user_name, strlen(user_name))) {
    return PROTOCOL_ERROR;
  }

  return SUCCESS;
}

error_code client_send_layout(client *client,
                              uint32_t num_vertices, uint32_t num_quads,
                              uint32_t num_hexes, uint32_t num_symmetries,
                              uint32_t num_boundary_vertices,
                              uint32_t max_no_goods_size, uint8_t flags) {
  if (!write_command(client_should_stay_open, client, client->socket,
                     TAG_LAYOUT, 0))
    return PROTOCOL_ERROR;

  if (!write_layout(client_should_stay_open, client, client->socket,
                    num_vertices, num_quads, num_hexes,
                    num_symmetries, num_boundary_vertices,
                    max_no_goods_size, flags))
    return PROTOCOL_ERROR;

  compute_subproblem_layout(&client->layout,
                            num_vertices, num_quads, num_hexes,
                            num_symmetries, num_boundary_vertices,
                            max_no_goods_size, flags);
  if (!(client->buffer = malloc(client->layout.packet_size)))
    return OUT_OF_MEMORY;

  return SUCCESS;
}

error_code client_receive_layout(client *client,
                                 uint32_t *num_vertices, uint32_t *num_quads,
                                 uint32_t *num_hexes, uint32_t *num_symmetries,
                                 uint32_t *num_boundary_vertices,
                                 uint32_t * max_no_goods_size,
                                 uint8_t *flags) {
  message_tag tag;
  uint64_t arg;
  if (!read_command(client_should_stay_open, client, client->socket,
                    &tag, &arg) || tag != TAG_LAYOUT)
    return PROTOCOL_ERROR;

  if (!read_layout(client_should_stay_open, client, client->socket,
                   num_vertices, num_quads, num_hexes,
                   num_symmetries, num_boundary_vertices,
                   max_no_goods_size, flags))
    return PROTOCOL_ERROR;

  compute_subproblem_layout(&client->layout,
                            *num_vertices, *num_quads, *num_hexes,
                            *num_symmetries, *num_boundary_vertices,
                            *max_no_goods_size, *flags);

  if (client->buffer) {
    free(client->buffer);
    client->buffer = NULL;
  }

  if (!(client->buffer = malloc(client->layout.packet_size)))
    return OUT_OF_MEMORY;

  return SUCCESS;
}

error_code client_send_subproblems(client *client, uint32_t offset, uint32_t n,
                                   solver_pool *pool) {
  for (uint32_t i = 0; i < n; i++) {
    if (!write_command(client_should_stay_open, client,
                       client->socket, TAG_SUBPROBLEM, offset + i)) {
      return PROTOCOL_ERROR;
    }

    if (!write_subproblem(client_should_stay_open, client,
                          client->socket, client->buffer, &client->layout,
                          solver_pool_get(pool, offset + i))) {
      return PROTOCOL_ERROR;
    }
  }

  return SUCCESS;
}

error_code client_finish_send_subproblems(client *client) {
  if (!write_command(client_should_stay_open, client,
                     client->socket, TAG_EMPTY, 0))
    return PROTOCOL_ERROR;

  return SUCCESS;
}

error_code client_request(client *client, uint32_t offset, uint32_t n,
                          solver_pool *pool,
                          uint32_t *ids,
                          uint32_t *n_received) {
  if (!write_command(client_should_stay_open, client,
                     client->socket, TAG_REQUEST, n))
    return PROTOCOL_ERROR;

  message_tag tag;
  uint64_t arg;

  bool received_command = true;

  uint32_t i = 0;
  while ((received_command =
          read_command(client_should_stay_open, client,
                       client->socket, &tag, &arg)) &&
         tag != TAG_EMPTY) {
    if (tag != TAG_SUBPROBLEM)
      return PROTOCOL_ERROR;

    if (i == n)
      return PROTOCOL_ERROR;

    ids[offset + i] = arg;

    if (!read_subproblem(client_should_stay_open, client,
                         client->socket, client->buffer, &client->layout,
                         solver_pool_get(pool, offset + i)))
      return PROTOCOL_ERROR;

    i++;
  }

  *n_received = i;

  if (!received_command)
    return PROTOCOL_ERROR;
  else
    return SUCCESS;
}

error_code client_notify_finished(client *client, uint32_t i) {
  if (!write_command(client_should_stay_open, client,
                     client->socket, TAG_FINISHED, i))
    return SYSTEM_ERROR;

  return SUCCESS;
}

error_code client_send_solution(client *client, vertex_index *vertices,
                                hex_index num_hexes) {
  if (8 * num_hexes > client->layout.packet_size)
    return PROTOCOL_ERROR;

  if (!write_command(client_should_stay_open, client,
                     client->socket, TAG_SOLUTION, 0))
    return PROTOCOL_ERROR;

  if (!write_solution(client_should_stay_open, client, client->socket,
                      client->buffer, vertices, num_hexes))
    return PROTOCOL_ERROR;

  return SUCCESS;
}

bool client_idle(client *client, bool *is_asleep,
                 bool (*cond)(void *user_data), void *user_data) {
  message_tag tag;
  uint64_t arg;
  if (read_command(cond, user_data, client->socket, &tag, &arg)) {
    if (tag == TAG_WAKEUP && *is_asleep) {
      *is_asleep = false;
      return true;
    }
    else
      return false;
  }

  return !cond(user_data);
}

void client_close(client *client) {
  if (client->buffer) free(client->buffer);
  close(client->socket);
}
